# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jaewon

import logging

from coin.base.timestamp import get_timestamp
from coin.exchange.base.order_gateway import OrderSide, OrderType
from coin.exchange.kr_rest.product_holders import get_holder_from_product


class BitflyerSimpleMMExecutor(object):
  def __init__(self, execution_params, order_gateway, logger=None):
    self._logger = logger or logging.getLogger('BitflyerSimpleMMExecutor')
    self._og = order_gateway

    self._product = execution_params['product']
    self._product_holder = get_holder_from_product(self._product)
    self._pos_bias = float(execution_params.get('pos_bias', 0.))
    self._logger.info('Position bias: %f', self._pos_bias)

    self._lot_size = execution_params['lot_size']
    self._stickiness = execution_params['stickiness']
    self._min_pos = execution_params['min_pos']
    self._max_pos = execution_params['max_pos']
    self._min_order_qty = execution_params.get('min_order_qty', 0.2)
    self._order_update_period = (execution_params.get('order_update_period', 2.) * (10**9))
    self._do_not_submit_after_cancel = (execution_params.get('do_not_submit_after_cancel', False))

    self._position = None
    self._sell_close_working_qty = 0  # includes on-the-fly, not zombie
    self._buy_close_working_qty = 0  # includes on-the-fly, not zombie

    self._sell_on_the_fly_orders = []
    self._sell_working_orders = []
    self._buy_on_the_fly_orders = []
    self._buy_working_orders = []

    self._cancel_list = []
    self._sell_price = None
    self._buy_price = None
    self._last_fire_time = 0

  def _update_state(self):
    self._position = self._og.get_position(self._product) + self._pos_bias

    self._sell_on_the_fly_orders = []
    self._sell_working_orders = []
    self._buy_on_the_fly_orders = []
    self._buy_working_orders = []

    for order in self._og.get_working_order():
      if order.product != self._product:
        continue

      if order.order_side == OrderSide.SELL:
        if order.internal.in_matching_queue:
          self._sell_working_orders.append(order)
        else:
          self._sell_on_the_fly_orders.append(order)

      elif order.order_side == OrderSide.BUY:
        if order.internal.in_matching_queue:
          self._buy_working_orders.append(order)
        else:
          self._buy_on_the_fly_orders.append(order)

      else:
        self._logger.error('Unknown side: %s', str(order))
        continue

  def _cancel_sell_working_orders(self, exclude_order_id=None):
    for order in self._sell_working_orders:
      if order.order_id == exclude_order_id:
        continue
      self._cancel_list.append(order.order_id)

  def _cancel_buy_working_orders(self, exclude_order_id=None):
    for order in self._buy_working_orders:
      if order.order_id == exclude_order_id:
        continue
      self._cancel_list.append(order.order_id)

  def _submit_sell_order(self, sell_price):
    self._sell_price = sell_price

  def _submit_buy_order(self, buy_price):
    self._buy_price = buy_price

  def _update_sell_order(self, sell_price):
    # If there is an on-the-fly order, cancel all working orders and keep the
    # on-the-fly order.
    if (sell_price is None or self._sell_on_the_fly_orders or self._position <= self._min_pos):
      self._cancel_sell_working_orders()
      return

    # Create new order if there is no working order.
    if not self._sell_working_orders:
      self._submit_sell_order(sell_price)
      return

    # If there is an working order that its price is same to sell_price,
    # keep it and cancel all other orders.
    for order in self._sell_working_orders:
      if abs(order.price - sell_price) <= self._stickiness:
        self._cancel_sell_working_orders(exclude_order_id=order.order_id)
        return

    # Cancel all other orders and place a new order.
    if self._sell_working_orders:
      self._cancel_sell_working_orders()
      if self._do_not_submit_after_cancel:
        self._logger.debug('No sell due to cancel')
      else:
        self._submit_sell_order(sell_price)
    else:
      self._submit_sell_order(sell_price)

  def _update_buy_order(self, buy_price):
    # If there is an on-the-fly order, cancel all working orders and keep the
    # on-the-fly order.
    if (buy_price is None or self._buy_on_the_fly_orders or self._position >= self._max_pos):
      self._cancel_buy_working_orders()
      return

    # Create new order if there is no working order.
    if not self._buy_working_orders:
      self._submit_buy_order(buy_price)
      return

    # If there is an working order that its price is same to buy_price,
    # keep it and cancel all other orders.
    for order in self._buy_working_orders:
      if abs(order.price - buy_price) <= self._stickiness:
        self._cancel_buy_working_orders(exclude_order_id=order.order_id)
        return

    # Cancel all working orders and place new one.
    if self._buy_working_orders:
      self._cancel_buy_working_orders()
      if self._do_not_submit_after_cancel:
        self._logger.debug('No buy due to cancel')
      else:
        self._submit_buy_order(buy_price)
    else:
      self._submit_buy_order(buy_price)

  def sanitize_qty(self, qty):
    assert qty > 0, qty
    adj_qty = self._product_holder.get_formatter().floor_qty(qty)
    return adj_qty

  def manage_mm_orders(self, sell_price, buy_price):
    if get_timestamp() - self._last_fire_time < self._order_update_period:
      return False

    self._cancel_list = []
    self._sell_price = None
    self._buy_price = None

    self._update_state()
    self._update_sell_order(sell_price)
    self._update_buy_order(buy_price)

    # Cancel first and submit order(s).
    if self._cancel_list:
      self._og.cancel_multiple(self._cancel_list)

    if self._sell_price:
      assert self._position > self._min_pos, self._position
      sell_qty = self.sanitize_qty(min(self._position - self._min_pos, self._lot_size))
      if sell_qty >= self._min_order_qty:
        self._og.submit(self._product, self._sell_price, sell_qty, OrderSide.SELL, OrderType.LIMIT)
        self._logger.info('SELL %s %f @ %.1f', self._product.symbol, sell_qty, self._sell_price)

    if self._buy_price:
      assert self._max_pos > self._position, self._position
      buy_qty = self.sanitize_qty(min(self._max_pos - self._position, self._lot_size))
      if buy_qty >= self._min_order_qty:
        self._og.submit(self._product, self._buy_price, buy_qty, OrderSide.BUY, OrderType.LIMIT)
        self._logger.info('BUY  %s %f @ %.1f', self._product.symbol, buy_qty, self._buy_price)

    if self._cancel_list or self._sell_price or self._buy_price:
      self._last_fire_time = get_timestamp()
      return True
    return False
